Skip to content

feat(client): analytics search has scope chips and shared highlight - #506

Open
jasnoorgill wants to merge 2 commits into
tashfeenahmed:mainfrom
jasnoorgill:feat/analytics-search-scope-chips
Open

feat(client): analytics search has scope chips and shared highlight#506
jasnoorgill wants to merge 2 commits into
tashfeenahmed:mainfrom
jasnoorgill:feat/analytics-search-scope-chips

Conversation

@jasnoorgill

Copy link
Copy Markdown
Contributor

feat(client): analytics search has scope chips and shared highlight

Summary

The analytics-page search box now filters the four list tables with a scope chip selector (All / Models / Calls / Errors / Keys) and a shared highlight ring on the input row + matched panel. The search is not persisted, charts and summary cards stay unfiltered.

This bundles two commits:

  • 468f033 — feat(client): analytics search has scope chips and shared highlight (the feature)
  • 3039d4c — fix(client): bolder analytics search highlight (a 2-line visual tweak on top)

Motivation

The previous state had the search box filtering every list table at once, with no way to narrow scope. A user typing an IP got four partial results lists, half of which were irrelevant for what they were inspecting. There was also no visual feedback for "which table is this filter actually affecting?" — the user had to read every panel header to know.

Two specific user pain points this fixes:

  1. Deep-link / reload surprise. Search persisted (or didn't, depending on the table), so a deep link to /analytics?q=gpt-4 would land in a half-filtered state with no context. Now the search resets to empty on every page mount, and the chips give an explicit "scope" for the current query.

  2. No visual grouping of "active filter". When the search matched rows in, say, the recent-calls table only, there was no visual cue tying the input box to that table. Now the input row and the matched panel share the same primary-tinted ring + shadow + 200ms transition, so they pulse in sync and read as one visual unit.

Changes

  • client/src/pages/AnalyticsPage.tsx (+339/-56, biggest file in the diff):

    • Imports: useMemo from react, Search + X from lucide-react, cn from @/lib/utils.
    • makeMatches<T>(getHaystack) — generic helper that lower-cases a row's haystack and substring-matches the trimmed query. Empty query short-circuits to true so the source array stays untouched in the common no-filter case.
    • Panel wrapper — gained optional highlighted and countLabel props. Highlighted adds a ring-2 ring-primary/50 shadow-lg shadow-primary/10 (later bumped to ring-4 ring-primary shadow-xl shadow-primary/30 in the follow-up) and a 200ms transition-shadow. Eight non-list panels (charts, summary, per-platform breakdowns) keep their old shape unchanged.
    • State: search: string, scope: 'all' | 'models' | 'calls' | 'errors' | 'keys' (default 'all'), trimmedQuery (lowered + trimmed once), hasQuery.
    • Per-table filters: matchesByModel, matchesByKey, matchesRecentCall, matchesRecentError (useMemoized for stable identity). visibleByModel, visibleErrors, visibleByKey, visibleRecentCalls (filtered arrays). highlightByModel, highlightErrors, highlightByKey, highlightByCalls (each is hasQuery && inScope(kind) && source.length > 0 && visible.length > 0). anyPanelHighlighted is the OR of the four — drives the input row's ring.
    • Toolbar UI: the <SegmentedControl> (range) is wrapped in a flex row with a search-input + scope-chip strip on the left. The chip strip uses role="group" + aria-label="Filter scope" for screen readers; each chip is a real <button> with aria-pressed. Selected chip uses bg-primary text-primary-foreground border-primary, mirroring the panel ring's primary tint.
    • Four list Panel invocations wired: recentErrors, recentCalls, perModelBreakdown, usageByKey. Each got:
      • highlighted={highlight*}
      • countLabel={highlight* ? t('analytics.matchedCount', { shown: visible*.length, total: source.length }) : undefined}
      • The data source switched from errors / recentCalls.rows / byModel / byKey to the matching visible* array
      • A visible*.length === 0 empty-state branch that renders t('analytics.noMatches') (separate from the "no data at all" branch which keeps the old copy).
  • Six i18n locale files (en, es, fr, it, pt-BR, zh-CN): +9 keys each

    • searchAriaLabel (input aria-label)
    • searchScopeAriaLabel (chip strip group label)
    • searchScopeAll/Models/Calls/Errors/Keys (chip labels)
    • matchedCount ("{shown} of {total}")
    • noMatches (empty-state copy)

Test plan

  • npm run build — full build succeeds (server + client), bundle hash index-Bxql93PJ.js includes "Filter scope" (×1), searchScope* (×6 keys), matchedCount (×10 = 4 countLabel invocations + i18n key + deps), ring-4 ring-primary (×2 = input + Panel wrapper), ring-2 ring-primary/50 (×0 = old style fully replaced).
  • npm test — 94 test files, 926 tests passed.
  • Visual check (local integration branch, on the live service):
    • Type a query → all four list tables filter simultaneously with the "All" chip active.
    • Click "Models" chip → only the per-model table filters; recent-errors, recent-calls, usage-by-key render unfiltered.
    • Each in-scope panel with ≥1 match gets the same primary ring + shadow, and the input row's ring animates in sync.
    • Type a query with zero matches in a panel → that panel renders t('analytics.noMatches') (no highlight, no count); other in-scope panels with matches still highlight.
    • Reload the page → search resets to empty (NOT persisted), scope chip stays at "All" (default).
    • Charts, summary stats, and time-series panels are unaffected by the search.

Compatibility

  • No server changes — search is purely client-side over already-fetched rows. The /api/analytics/* endpoints and their response shapes are unchanged.
  • No new dependencieslucide-react and cn from @/lib/utils are already used elsewhere in the client (EmbeddingDetailPage, EmbeddingsPage, etc.).
  • i18n additive only — nine new keys per locale. Existing translations untouched; the chips degrade gracefully if a locale ever drops a key (renders the raw key, same as the rest of the app).
  • No new public API or breaking changes to existing components. The Panel signature change is backward-compatible: highlighted and countLabel are optional, default false / undefined, and the eight non-list call sites are unchanged.
  • Reuses FallbackPage's search-row shape (per skill discipline, cherry-pick-pr-scope-discipline): same rounded container, same w-56 input width, same scope-on-the-right pattern. Power users get one mental model across the dashboard.

Out of scope (deliberate)

The integration branch (integration/all-my-prs) also carries:

  • Per-model sortable column headers
  • Recent-calls sortable column headers
  • Usage-by-key sortable column headers
  • Persist selected time range in localStorage
  • A few smaller analytics tweaks

These are separate features with their own review shape. Keeping this PR focused on the search/scope-chips/highlight bundle keeps the diff reviewable (7 files, +353/-56) and lets each feature land independently upstream. Happy to ship any of those as follow-up PRs if wanted.

Follow-up commit (3039d4c)

A 2-line visual tweak shipped as a follow-up commit on top:

  • ring-2 ring-primary/50 shadow-lg shadow-primary/10ring-4 ring-primary shadow-xl shadow-primary/30
  • Applied identically to the input row and the Panel wrapper

This makes the highlight pop more obviously against the dashboard background. Keeping it as a separate commit makes the bump easy to revert if reviewers want a more subtle effect.

The analytics-page search box used to filter every list-driven table at
once. That made the page land in a half-filtered state from a deep
link, and stacked empty states across four tables when the query had
zero matches.

Three changes:

1. Search input + scope chip strip share one rounded container. Five
   chips (All / Models / Calls / Errors / Keys, default All) drive
   which tables the active query applies to. Tables outside the
   active scope render unfiltered; the user is never surprised by a
   panel shrinking when they typed something.

2. The container and the four list panels share the same highlight
   ring + shadow + 200ms transition, so the input area and the
   filtered table read as one visual unit. A panel highlights only
   when its scope is active AND the filter produced ≥1 match; the
   highlight drives both the ring around the card and a small
   "<shown> of <total>" count in the panel header.

3. The Panel wrapper grows an optional highlighted + countLabel prop,
   so charts and other non-list surfaces keep their old shape
   unchanged. The four list panels (per-model, recent-calls, recent-
   errors, usage-by-key) wire the new props + filtered array; the
   other eight panels ignore them.

Filter applies client-side over already-fetched rows so keystrokes
don't re-hit the API. Search is not persisted (resets to empty on
mount) — same shape as FallbackPage and ProviderList, and avoids
landing in a half-filtered state from a deep link.

Charts, summary stat cards, and time-series are deliberately NOT
filtered: they aggregate over the selected window and filtering them
would misrepresent totals. The user's search is for inspecting
specific rows, not the aggregate.

i18n: nine new keys (searchAriaLabel, searchScopeAriaLabel,
searchScopeAll/Models/Calls/Errors/Keys, matchedCount, noMatches)
across all six locales (en, es, fr, it, pt-BR, zh-CN).

This is the slim scope from the integration branch: 7 files, +353/-56.
The per-model/byKey/recent-calls sortable column headers stay on the
integration branch for a separate PR if desired.
Bump shared highlight on the search input and the four table panels
from ring-2 ring-primary/50 + shadow-lg/10 to ring-4 ring-primary +
shadow-xl/30. Both pulse in sync on the same 200ms transition so the
input-area + matched panels read as a single grouping.

Applied identically to the input row and the Panel wrapper so they
stay visually matched; the only difference is the wrapper's
rounded-3xl vs the input's rounded-xl which is intentional (the
panels are larger cards).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant